package lv.rtu.ditf.dc.thread;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import lv.rtu.ditf.dc.classification.ClassificationUtils;
import lv.rtu.ditf.dc.fs.Document;
import lv.rtu.ditf.dc.fs.DocumentCategory;
import lv.rtu.ditf.dc.fs.FileStorage;
import lv.rtu.ditf.dc.hashmap.routines.HashMapRoutines;
import lv.rtu.ditf.dc.keyvalue.CategorizationClassPointComparator;
import lv.rtu.ditf.dc.keyvalue.CategorizationClassPointKeyValue;

/**
 * 
 * @author Pāvels Koliškins, Edgars Matisāns
 *
 */
public class CategorizationThread extends Thread {

    private HashMap<String, Integer> documentToClassifyVector;
    private final int kNNParameter;
    private final double epsilon;
    private long executionStartTime;
    
    private Set<CategorizationDistanceCallback> categorizationDistanceListeners;
    private Set<CategorizationCallback> categorizationListeners;

    public HashMap<String, Integer> getDocumentToClassifyVector() {
        return documentToClassifyVector;
    }

    public void setDocumentToClassifyVector(HashMap<String, Integer> testDocumentVector) {
        this.documentToClassifyVector = testDocumentVector;
    }
    
    public CategorizationThread(HashMap<String, Integer> testDocumentVector, int kNNParameter, double epsilon) {
        this.documentToClassifyVector = testDocumentVector;
        this.kNNParameter = kNNParameter;
        this.epsilon = epsilon;
        this.executionStartTime = 0;
    }

    @Override
    public void run() {
        this.executionStartTime = System.currentTimeMillis();

        ArrayList<CategorizationClassPointKeyValue> categorizationResults = new ArrayList<CategorizationClassPointKeyValue>();
        try {
            FileStorage fileStorage = FileStorage.get();
            List<DocumentCategory> documentCategories = fileStorage.getDocumentCategories();
            int totalDocumentCount = fileStorage.getTotalDocumentCount();
            HashMap<String, Integer> docCountByTerm = fileStorage.getDocumentCountByTerm();

            // itterate through all categories and all files in each category
            // calculate cosine simillarities between test document and
            // all available documents in system
            for (DocumentCategory documentCategory : documentCategories) {
            	
                List<Document> categoriesDocuments = documentCategory.getDocuments();
                for (Document categoryDocument : categoriesDocuments) {

                    double distance = ClassificationUtils.getCosineSimilarity(
                            getDocumentToClassifyVector(), 
                            categoryDocument.getTermWeights(totalDocumentCount, docCountByTerm));

                    // if we sum NaN and number we get NaN in result
                    if (Double.isNaN(distance)) distance = 0.0;
                        
                    categorizationResults.add(
                            new CategorizationClassPointKeyValue(documentCategory.getName(), distance));
                }
            }

            // sort ascending cosine similarities
            Collections.sort(categorizationResults, new CategorizationClassPointComparator());
            
            // leave only kNN values in list
            // kNN is guaranteed to be less than MAX_DOCUMENTS
            List<CategorizationClassPointKeyValue> categorizationResultSubList = categorizationResults.subList(0, kNNParameter);
                        
            // if first element in list is less than EPSILON value
            // test document cannot be categorized
            if (categorizationResultSubList.size() > 0) {
                if (categorizationResultSubList.get(0).getDistancePoint() < this.epsilon) {
                    categorizationResultSubList.clear();
                }
            }
            
            // notify listeners about distance calculation status
            categorizationDistancesCalculated(categorizationResultSubList, (System.currentTimeMillis() - executionStartTime));
            
            // calculate final similarities
            double pointsCollected = 0d;
            Map<String, DocumentCategory> categoriesByNames = FileStorage.get().getCategoriesByNames(); 
            
            HashMap<String, Double> statisticHashMap = new HashMap<String, Double>();
            for (CategorizationClassPointKeyValue categorizationClassPointKeyValue : categorizationResultSubList) {
            	// normalize distance according to document count in category
            	double normalizedDistance = categorizationClassPointKeyValue.getDistancePoint() / 
            		categoriesByNames.get(categorizationClassPointKeyValue.getCategorizationClass()).getDocuments().size();
            	
                if (statisticHashMap.get(categorizationClassPointKeyValue.getCategorizationClass()) == null) {
                    statisticHashMap.put(categorizationClassPointKeyValue.getCategorizationClass(), 
                    		normalizedDistance);
                } else {
                    statisticHashMap.put(categorizationClassPointKeyValue.getCategorizationClass(), 
                            statisticHashMap.get(categorizationClassPointKeyValue.getCategorizationClass()) + 
                            normalizedDistance);
                }
                
                pointsCollected += normalizedDistance;
            }
            
            //get percents for each category
            //percent = sum of category cosine values / sum of all cosine values
            
            for (String categoryName : statisticHashMap.keySet()) {
                statisticHashMap.put(categoryName, 
                        statisticHashMap.get(categoryName) / pointsCollected);
            }
                        
            //sorting hash map with percents
            List<String> sortedKeys = HashMapRoutines.sortByValueDesc(statisticHashMap);
            
            LinkedHashMap<String, Double> orderedResultMap = new LinkedHashMap<String, Double>();
            for (String key : sortedKeys)
            	orderedResultMap.put(key, statisticHashMap.get(key));
            
            // notify listeners about categorization status
            categorizationCompleted(orderedResultMap, (System.currentTimeMillis() - executionStartTime));
            
        } catch (Exception ex) {
            ex.printStackTrace();
            categorizationDistancesCalculated(null, (System.currentTimeMillis() - executionStartTime));
            categorizationCompleted(null, (System.currentTimeMillis() - executionStartTime));
        }
    }
    
	public void addCategorizationDistanceListener(CategorizationDistanceCallback listener) {
		if (categorizationDistanceListeners == null)
			categorizationDistanceListeners = new HashSet<CategorizationDistanceCallback>();
		
		categorizationDistanceListeners.add(listener);
	}
	
	public void removeCategorizationDistanceListener(CategorizationDistanceCallback listener) {
		if (categorizationDistanceListeners != null)
			categorizationDistanceListeners.remove(listener);
	}
	
	public void addCategorizationListener(CategorizationCallback listener) {
		if (categorizationListeners == null)
			categorizationListeners = new HashSet<CategorizationCallback>();
		
		categorizationListeners.add(listener);
	}
	
	public void removeCategorizationListener(CategorizationCallback listener) {
		if (categorizationListeners != null)
			categorizationListeners.remove(listener);
	}
	
	private void categorizationDistancesCalculated(List<CategorizationClassPointKeyValue> results, long executionTime) {	
		if (categorizationDistanceListeners != null) {
			if (results != null)
				results = Collections.unmodifiableList(results);
			
			for (CategorizationDistanceCallback listener : categorizationDistanceListeners)
				listener.onDistanceCalculated(results, executionTime);
		}
	}
	
	private void categorizationCompleted(Map<String, Double> categoriesSimilarities, long executionTime) {
		if (categorizationListeners != null) {
			if (categoriesSimilarities != null)
				categoriesSimilarities = Collections.unmodifiableMap(categoriesSimilarities);
			
			for (CategorizationCallback listener : categorizationListeners)
				listener.onCategorizationComplete(categoriesSimilarities, executionTime);
		}
	}
}
